package gameStates;

import fileHandling.MapLoader;
import gameStates.absGamesStates.AbsIngameState;
import gameStates.absGamesStates.gui.AbsSpawnState;
import gameStates.gui.ChatState;
import gameStates.gui.HUDState;
import gameStates.gui.SpawnMenuState;
import gameStates.passes.AbsPassManagerState;
import input.Mouse;
import input.PlayerKeyInput;
import input.pilot.CameraNodeController;
import input.pilot.PilotKeyInput;
import input.pilot.PilotMouseInput;
import interfaces.hud.pilot.PilotHUD;
import interfaces.tips.TipsManager;

import java.util.List;
import java.util.logging.Logger;

import logic.DelayedController;
import logic.common.game.Game;
import logic.common.player.Player;
import logic.common.player.playerListeners.PlayerTeamListener;
import logic.common.player.playerListeners.PlayerValueListener;
import logic.common.team.Team;
import logic.nodes.collision.CollidableNode;
import logic.nodes.lod.blocks.LeafBlock;
import logic.ships.hunter.Hunter;
import logic.ships.mothership.MotherShip;
import logic.status.StatusChangedEvent;
import logic.status.StatusListener;
import logic.weapons.Weapon;
import logic.weapons.WeaponType;
import main.InitGame;
import main.listeners.listenerClasses.PauseListener;
import main.listeners.listenerClasses.SettingsListener;
import main.listeners.listenerClasses.gui.ChatActivatedEvent;
import main.listeners.listenerClasses.gui.ConsoleToggleEvent;
import main.listeners.listenerClasses.gui.GUIListener;
import main.listeners.listenerClasses.gui.IngameChangedEvent;
import main.listeners.listenerClasses.gui.ScoreBoardToggleEvent;
import main.listeners.listenerClasses.gui.SpawnMenuChangedEvent;
import map.Map;
import map.SpaceBackground;
import map.Stardust;
import map.asteroids.Asteroid;
import map.infos.LightMapInfos;
import map.spawnStation.SpawnStation;
import networking.messages.syncMessages.ValueType;

import org.fenggui.IWidget;

import settings.CommonSettings;
import settings.Config;
import settings.ControlSettings;
import sound.AccelerateController;
import sound.AudioManager;
import sound.DeccelerateController;

import com.jme.input.MouseInput;
import com.jme.light.DirectionalLight;
import com.jme.renderer.Camera;
import com.jme.renderer.Renderer;
import com.jme.scene.CameraNode;
import com.jme.scene.Controller;
import com.jme.scene.state.CullState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.lwjgl.LWJGLTimer;

/**
 * Implements an {@link IngameState} that is capable of processing a {@link Game} and 
 * a {@link Map}.
 * 
 * @author Wasserleiche
 */
public class IngameState extends AbsIngameState {
	
	public static final Logger logger = Logger.getLogger("gameStates.IngameState");
	
	protected Controller openSpawnController;
	
	protected float oldSpeed;
	protected StatusListener hunterSpeedListener;
	protected AccelerateController accelerateController;
	protected DeccelerateController deccelerateController;
	protected boolean accelerating, deccelerating;
	
	protected DelayedController playerDiedController;
	
	/**
	 * Constructs a new MatchState-Object.
	 */
	public IngameState(AbsPassManagerState passManagerState) {
		super(passManagerState);
		
		addListenerToClearer(new GUIListener() {
			@Override
			public void ingameChanged(IngameChangedEvent event) {
				setActive(event.isIngame());
			}
			
			@Override
			public void consoleToggled(ConsoleToggleEvent event) {
				enableInput(!event.getValue());
			}
			
			@Override
			public void spawnMenuChanged(SpawnMenuChangedEvent event) {}
			@Override
			public void scoreBoardToggled(ScoreBoardToggleEvent event) {}
			@Override
			public void chatActivated(ChatActivatedEvent event) {}
		});
		
		addListenerToClearer(new SettingsListener() {
			@Override
			public void graphicSettingsChanged() {
				restore();
			}
			
			@Override
			public void controlSettingsChanged() {
				updateControlSettings();
			}
			
			@Override
			public void commonSettingsChanged() {
				setupCam();
				changeHunterVisibility();
			}
			
			@Override
			public void soundSettingsChanged() {}
		});
		
		addListenerToClearer(new PauseListener() {
			@Override
			public void pause(Boolean isPaused) {
				setPaused(isPaused.booleanValue());
			}
		});
		
		hunterSpeedListener = new StatusListener() {
			private static final float MIN = 0.05f;
			@Override
			public void valueChanged(StatusChangedEvent event) {
				if(event.getType() == ValueType.Speed) {
					float speed = event.getFloatValue();
					if(speed <= MIN) {
						AudioManager.get().removeController(deccelerateController);
						AudioManager.get().removeController(accelerateController);
						accelerating = false;
						deccelerating = false;
						oldSpeed = 0f;
					} else if(!accelerating && oldSpeed == 0f && speed > MIN) {
						accelerateController.init();
						AudioManager.get().addController(accelerateController);
						AudioManager.get().removeController(deccelerateController);
						accelerating = true;
						deccelerating = false;
						oldSpeed = speed;
					} else if(!deccelerating && oldSpeed > MIN && speed < oldSpeed && speed > MIN) {
						String firstSource = accelerateController.getSecondSourceName();
						String secondFile = player.getHunter().getEngineSound() + "/end.wav";
						deccelerateController = new DeccelerateController(firstSource, secondFile, player.getHunter());
						deccelerateController.init();
						AudioManager.get().addController(deccelerateController);
						AudioManager.get().removeController(accelerateController);
						deccelerating = true;
						accelerating = false;
						oldSpeed = speed;
					} else oldSpeed = speed;
				}
			}
		};
	}
	
	/**
	 * Initializes the given {@link Map}. Sets up the {@link Camera}, initializes the scene and 
	 * sets initialized to true.
	 * @param map The {@link Map} to be used by this IngameState.
	 */
	@Override
	public void initNewMap(Map map) {
		if(map == null) return;
		this.map = map;
		timer = new LWJGLTimer();
		DisplaySystem display = DisplaySystem.getDisplaySystem();
		Renderer renderer = display.getRenderer();
		
		cam = renderer.createCamera(display.getWidth(), display.getHeight());
		renderer.setCamera(cam);
		setupCam();
		map.setupSpectatePoint(cam);
		
		initScene();
		
		initialized = true;
	}
	
	/**
	 * Simply clears the Buffer of the {@link DisplaySystem}'s {@link Renderer} and lets the rootNode 
	 * be drawn by that {@link Renderer}.
	 */
	@Override
	public void render(float tpf) {
		Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
		renderer.clearBuffers();
		renderer.draw(rootNode);
	}
	
	/**
	 * Updates this MatchState. The {@link InitGame.hudState} will be updated every second if the {@link Game} is 
	 * running. If one {@link MotherShip} is destroyed, the {@link Game} will be terminated. Here the 
	 * main-input-handler is updated, too.
	 */
	@Override
	public void update(float tpf) {
		if(!isPaused) {
			if(InitGame.get().isServer()) {
				int time = (int)timer.getTimeInSeconds();
				int seconds = (game.getMap().getTime() * 60) - time;
//				int seconds = 10 - time;
				
				if(currentSeconds != seconds) {
					updateGameTime(seconds);
					
					if(game.isRunning()) updateRespawnTime();
					else updateEndTime(currentCoundDownTime - 1);
				}
				handleMouse();
			}
			
			input.update(tpf);
		}
		
		if(game.isRunning()) {
			if(!isPaused) {
				threadPool.startExecution(tpf);
				if(playerDiedController != null) playerDiedController.update(tpf);
				rootNode.updateGeometricState(tpf, true);
				rootNode.updateRenderState();
			}
		}
	}
	
	/**
	 * Initializes the scene. That includes attaching the {@link SpaceBackground} of the 
	 * {@link Map}, creating a {@link ZBufferState}, a {@link LightState} and 
	 * a {@link CullState}, attaching all {@link Asteroid}s and, if the {@link Map} is a 
	 * {@link Map}, all {@link SpawnStation}s.
	 */
	private void initScene() {
		rootNode.attachChild(map.getBackground());
		Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
		
		ZBufferState zbs = renderer.createZBufferState();
		zbs.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
		zbs.setEnabled(true);
		rootNode.setRenderState(zbs);
		
		LightMapInfos lightInfos = MapLoader.getLightMapInfos(map.getPath());
		
		DirectionalLight dr = new DirectionalLight();
		dr.setEnabled(true);
		dr.setDirection(lightInfos.getDirection());
		dr.setAmbient(lightInfos.getAmbient());
		dr.setDiffuse(lightInfos.getDiffuse());
		dr.setShadowCaster(true);
		
		lightState = renderer.createLightState();
		lightState.detachAll();
		lightState.attach(dr);
		lightState.setEnabled(true);
		rootNode.setRenderState(lightState);
		
		CullState cs = renderer.createCullState();
		cs.setCullFace(CullState.Face.Back);
		cs.setEnabled(true);
		
		map.initStarDust(getCam());
		Stardust starDust = map.getStardust();
		float middle = map.getDimension() / 2;
		starDust.getLocalTranslation().set(middle, 0f, middle);
		rootNode.attachChild(starDust);
		
		rootNode.updateGeometricState(0f, true);
		rootNode.updateRenderState();
	}
	
	@Override
	public void addNode(CollidableNode node) {
		assert(!(node instanceof Weapon));
		
		node.getLeafBlocks().clear();
		if(node.getBound() != null) {
			if(rootBlock.intersectsWith(node)) rootBlock.searchLeafBlocks(node);
			else outerBlock.addLeafNode(node);
		}
		
		if(node.getParent() != rootNode) rootNode.attachChild(node);
		assert(node.getParent() == rootNode && rootNode.hasChild(node));
	}
	
	/**
	 * Creates a new {@link Camera}. The latest field-of-view will be fetched from the 
	 * {@link CommonSettings} as well as the latest display-width and -height.
	 */
	@Override
	public void setupCam() {
		int fov = CommonSettings.get().getFOV();
		float displayAspect = (float)DisplaySystem.getDisplaySystem().getWidth() 
							/ (float)DisplaySystem.getDisplaySystem().getHeight();
		float far = map.getDimension() * 4f;
		cam.setFrustumPerspective(fov, displayAspect, 0.5f, far);
		cam.update();
	}
	
	@Override
	public boolean isInRootBlock(CollidableNode node) {
		if(rootBlock == null) return true;
		return rootBlock.intersectsWith(node);
	}
	
	/**
	 * Sets the cursor to the menu-cursor whenever the {@link Mouse} is over an HUD-Element. If so, the 
	 * ability of the {@link Player} to shoot will be disabled.
	 */
	private void handleMouse() {
		Hunter hunter = player.getHunter();
		
		if(hunter != null) {
			if(hudState.isActive()) {
				MouseInput m = MouseInput.get();
				int x = m.getXAbsolute();
				int y = m.getYAbsolute();
				IWidget w = hudState.getGUIDisplay().getWidget(x, y);
				boolean enableWeapons = w == null || w == hudState.getCurrentHUD().getBasicContent();
				hunter.enableWeapons(WeaponType.Energy, enableWeapons);
			} else hunter.enableWeapons(WeaponType.Energy, false);
		}
	}
	
	@Override
	public void initPlayerKeyInput() {
		clearInput();
		playerKeyInput = new PlayerKeyInput(rootNode, chatState, this);
		input.addToAttachedHandlers(playerKeyInput);
	}
	
	@Override
	public void initIngameWindows() {
		chatState = new ChatState(passManagerState, this);
		chatState.setActive(false);
	}
	
	public void killPlayer() {
		playerDiedController = new DelayedController(Config.get().getSpawnMenuDeathDelay()) {
			private static final long serialVersionUID = 1L;
			
			@Override
			public void delayedUpdate(float time) {
//				if(!spawnState.isActive()) InitGame.get().toggleSpawnMenu();
				hudState.setSpectatedHUD();
				hudState.nextPlayer(true);
				playerDiedController = null;
			}
		};
		
		initPlayerKeyInput();
	}
	
	/**
	 * Spawns a new {@link Player} with a new {@link Hunter} that he has chosen. First the {@link Player} is 
	 * tried to killed. Then the new {@link Player} is build. If this fails, this method returns. If not, the 
	 * HUD will be set to a {@link PilotHUD} and the {@link SpawnMenuState} will be deactivated. Finally the 
	 * pilot-inputs will be initialized.
	 */
	@Override
	public void spawnCurrentPlayer() {
		if(buildNewPilot()) {
			initPilotInput();
			setupCam();
		}
	}
	
	/**
	 * Builds the chosen {@link Hunter} and applies the {@link Camera} to it.
	 * @return true, if the {@link Player} was ready yet. false, else.
	 */
	private boolean buildNewPilot() {
		if(!spawnState.isReady()) return false;
		
		if(player.getHunter() != spawnState.getSelectedHunter()) 
			player.changeHunter(spawnState.getSelectedHunter());

		Hunter hunter = player.getHunter();
		
		player.setSpawnLocation(spawnState.getSelectedSpawnLocation());
		setCamNode(hunter);
		
		player.respawn();
		hudState.unRegisterTeamNode(hunter);
		
		hudState.updateMoney(player.getMoney());
		
		changeHunterVisibility();
		
//		oldSpeed = 0f;
//		
//		accelerating = false;
//		deccelerating = false;
//		
//		String start = hunter.getEngineSound() + "/start.wav";
//		String main = hunter.getEngineSound() + "/main.wav";
//		
//		accelerateController = new AccelerateController(start, main, hunter);
//		
//		hunter.removeStatusListener(hunterSpeedListener);
//		hunter.addStatusListener(hunterSpeedListener);
		
		return true;
	}
	
	@Override
	public void setCamNode(Hunter hunter) {
		initCamNode();
		hunter.attachChild(camNode);
	}
	
	/**
	 * Initializes the {@link CameraNode}. If there is already one, it will be removed. The local 
	 * translation will be set to (0.0f, 2.3f, -9f).
	 */
	private void initCamNode() {
		if(camNode != null) camNode.removeFromParent();
		camNode = new CameraNode("Cam Node", cam);
		camNode.getLocalTranslation().set(CameraNodeController.DEFAULT_CAM_NODE_LOCATION);
	}
	
	/**
	 * Initializes the input of an pilot. If the key-input or the mouse-input are already initialized, 
	 * they will be removed. This method simply creates an {link PilotKeyInput}- and an 
	 * {@link PilotMouseInput}-Object and applies them to the main-input-handler.
	 */
	private void initPilotInput() {
		clearInput();
		
		Hunter hunter = player.getHunter();
		
		playerKeyInput = new PilotKeyInput(hunter, this, chatState);
		input.addToAttachedHandlers(playerKeyInput);
		playerMouseInput = new PilotMouseInput(this);
		input.addToAttachedHandlers(playerMouseInput);
	}
	
	/**
	 * Updates the control-settings to the latest {@link ControlSettings}, if this MatchState is active.
	 */
	public void updateControlSettings() {
		if(playerKeyInput != null) playerKeyInput.changeControlSettings();
		if(playerMouseInput != null) playerMouseInput.changeControlSettings();
	}
	
	/**
	 * Restores this MatchState. This method sets the mouse-cursor to the ingame-crosshair and sets up 
	 * date input according the the current {@link Player}'s status.
	 */
	@Override
	public void restore() {
		super.restore();
		if(!isInitialized()) return;
		
		if(player.getHunter() != null && player.isAlive()) initPilotInput();
	}
	
	@Override
	public void setPlayer(Player p) {
		this.player = p;
		
		p.addPlayerValueListener(new PlayerValueListener() {
			@Override
			public void valueChanged(StatusChangedEvent event) {
				if(event.getType() == ValueType.Deaths) killPlayer();
			}
		});
		
		p.addPlayerTeamListener(new PlayerTeamListener() {
			@Override
			public void teamChanged(Team newTeam) {
				int i = 0;
				List<CollidableNode> nodes = null;
				
				for(LeafBlock block : LeafBlock.allLeafBlocks) {
					if(block.getLeafNodes().isEmpty()) continue;
					
					nodes = block.getNoneWeaponFireNodes();
					for(i = 0; i < nodes.size(); i++) {
						CollidableNode node = nodes.get(i);
						if(node != null) node.checkPlayerVisibility();
					}
				}
			}
			
			@Override
			public void teamChangeFailed() {}
		});
		
		game.setCurrentPlayer(p);
		
		hudState = new HUDState(this, passManagerState);
		hudState.setActive(false);
	}
	
	private void changeHunterVisibility() {
		boolean showHunter = CommonSettings.get().showHunter();
		
		Hunter hunter = player.getHunter();
		if(hunter != null) {
			if(showHunter) hunter.attachModel();
			else hunter.detachModel();
		}
	}
	
	@Override
	public void setSpawnMenuState(AbsSpawnState spawnState) {
		super.setSpawnMenuState(spawnState);
		
		if(Config.get().showTips()) {
			TipsManager.init();
			TipsManager.get().setupPlayer(player, spawnState, hudState);
		}
	
	}
	
	/**
	 * Returns the current {@link CameraNode}. If it is null, it will be initialized.
	 * @return The {@link CameraNode}. Cannot be null.
	 */
	@Override
	public CameraNode getCamNode() {
		if(camNode == null) initCamNode();
		return camNode;
	}
}